﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Text;

using NMemcached.Client.Configuration;

namespace NMemcached.Client
{
    /// <summary>
    /// An <see cref="System.Runtime.Caching.ObjectCache"/> implementation that uses Memcached as the backing store
    /// </summary>
    /// <remarks>
    /// <para>Memcached does not support cache regions. Any value passed to the region arguments are ignored.</para>
    /// </remarks>
    public class MemcachedObjectCache : ObjectCache
    {
        /// <summary>
        /// The expiration for cache items set in the class indexer. The default value is 15 minutes.
        /// </summary>
        public static readonly TimeSpan DefaultExpiration;

        private static readonly DefaultCacheCapabilities Capabilities;
        private static readonly string CacheName;

        private MemcachedClient _client;

        /// <summary>
        /// Gets the description of the features the ObjectCache provides.
        /// </summary>
        public override DefaultCacheCapabilities DefaultCacheCapabilities
        {
            get { return MemcachedObjectCache.Capabilities; }
        }

        /// <summary>
        /// Gets the name of the MemcachedObjectCache instance
        /// </summary>
        public override string Name
        {
            get { return MemcachedObjectCache.CacheName; }
        }

        /// <summary>
        /// Gets or sets the default indexer for the MemcachedObjectCache class
        /// </summary>
        /// <param name="key">The unique identifier for an entry in the cache</param>
        /// <returns>The value of the specified cache entry; or, null if no item exists with the specified key</returns>
        /// <exception cref="System.ArgumentNullException">key is null or white space</exception>
        public override object this[string key]
        {
            get
            {
                return this._client.Get(key);
            }
            set
            {
                this._client.Set(key, value, DateTime.Now.Add(MemcachedObjectCache.DefaultExpiration));
            }
        }

        static MemcachedObjectCache()
        {
            MemcachedObjectCache.DefaultExpiration = TimeSpan.FromMinutes(15);
            MemcachedObjectCache.Capabilities = DefaultCacheCapabilities.AbsoluteExpirations | DefaultCacheCapabilities.OutOfProcessProvider;
            MemcachedObjectCache.CacheName = StringTable.Get("runtime_CacheObjectName");
        }

        /// <summary>
        /// Initializes a new instance of the MemcachedObjectCache class
        /// </summary>
        /// <param name="connectionProvider">Optional. An IConnectionProvider used to communicate with the Memcached server(s)</param>
        public MemcachedObjectCache(IConnectionProvider connectionProvider = null)
        {
            if (connectionProvider == null)
            {
                this._client = NMemcachedConfiguration.CreateClient();
            }
            else
            {
                this._client = new MemcachedClient(connectionProvider);
            }
        }

        /// <summary>
        /// Adds a new item to the cache or returns the cached item if the specified key already exists.
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="value">The object to be cached</param>
        /// <param name="policy">An object that contains eviction details for the cache entry</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>The value of the cache entry if it already exists; otherwise, null</returns>
        /// <exception cref="System.ArgumentNullException">key, value, or policy is null.</exception>
        public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            if (policy == null)
            {
                throw new ArgumentNullException("policy");
            }

            return this.AddOrGetExisting(key, value, policy.AbsoluteExpiration);
        }

        /// <summary>
        /// Adds a new item to the cache or returns the cached item if the specified key already exists.
        /// </summary>
        /// <param name="value">The object to insert</param>
        /// <param name="policy">An object that contains eviction details for the cache entry</param>
        /// <returns>The value of the cache entry if it already exists; otherwise, null</returns>
        /// <exception cref="System.ArgumentNullException">value or policy is null.</exception>
        public override CacheItem AddOrGetExisting(CacheItem value, CacheItemPolicy policy)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (policy == null)
            {
                throw new ArgumentNullException("policy");
            }

            object cachedValue = this.AddOrGetExisting(value.Key, value.Value, policy.AbsoluteExpiration);

            CacheItem item = null;

            if (cachedValue != null)
            {
                item = new CacheItem(value.Key, cachedValue);
            }

            return item;
        }

        /// <summary>
        /// Adds a new item to the cache or returns the cached item if the specified key already exists.
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="value">The object to be cached</param>
        /// <param name="absoluteExpiration">The fixed date and time the entry will expire.</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>The value of the cache entry if it already exists; otherwise, null</returns>
        /// <exception cref="System.ArgumentNullException">key, value, or absoluteExpiration is null</exception>
        public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            if (String.IsNullOrWhiteSpace(key))
            {
                throw new ArgumentNullException("key");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (absoluteExpiration == null)
            {
                throw new ArgumentNullException("aboluteExpiration");
            }

            object cachedValue = null;

            CacheDetail detail = this._client.GetWithDetail(key);
            if (detail != null)
            {
                cachedValue = detail.Value;
            }
            else
            {
                this._client.Add(key, value, absoluteExpiration.DateTime);
            }

            return cachedValue;
        }

        /// <summary>
        /// Checks whether the cache entry already exists in the cache
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>True if the cache item exists; otherwise, false</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override bool Contains(string key, string regionName = null)
        {
            return this.Get(key) != null;
        }

        /// <summary>
        /// Creates a CacheEntryChangeMonitor object that can trigger events in response to changes to specified cache entries
        /// </summary>
        /// <param name="keys">The unique identifiers for cache entries to monitor. </param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>A change monitor that monitors cache entries in the cache. </returns>
        /// <exception cref="System.NotSupportedException">Entry change monitoring is not supported by Memcached.</exception>
        /// <exception cref="System.ArgumentNullException">keys is null</exception>
        public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable<string> keys, string regionName = null)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Gets the specified cache entry
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>The cache entry if it exists; otherwise, null</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override object Get(string key, string regionName = null)
        {
            if (String.IsNullOrWhiteSpace(key))
            {
                throw new ArgumentNullException("key");
            }

            return this._client.Get(key);
        }

        /// <summary>
        /// Gets the specified cache entry
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>The cache entry if it exists; otherwise, null</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override CacheItem GetCacheItem(string key, string regionName = null)
        {
            if (String.IsNullOrWhiteSpace(key))
            {
                throw new ArgumentNullException("key");
            }

            CacheDetail detail = this._client.GetWithDetail(key);

            CacheItem item = null;

            if (detail != null)
            {
                item = new CacheItem(detail.Key, detail.Value);
            }

            return item;
        }

        /// <summary>
        /// Gets the count of items in the cache.
        /// </summary>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added.</param>
        /// <returns>The number of cache entries in the cache.</returns>
        /// <exception cref="System.NotSupportedException">Cache entry counts are not supported by Memcached</exception>
        public override long GetCount(string regionName = null)
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Creates an enumerator that can be used to iterate through a collection of cache entries. 
        /// </summary>
        /// <returns>The enumerator object that provides access to the cache entries in the cache.</returns>
        /// <exception cref="System.NotSupportedException">Enumerating cache entries is not supported by Memcached</exception>
        protected override IEnumerator<KeyValuePair<string, object>> GetEnumerator()
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Gets a set of cache entries that correspond to the specified keys
        /// </summary>
        /// <param name="keys">A colleciton of unique identifiers for the cache entries to get</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added</param>
        /// <returns>A dictionary of key/value pairs that represent cache entries. </returns>
        /// <exception cref="System.ArgumentNullException">keys is null</exception>
        public override IDictionary<string, object> GetValues(IEnumerable<string> keys, string regionName = null)
        {
            if (keys == null)
            {
                throw new ArgumentNullException("keys");
            }

            return this._client.Get(keys.ToArray());
        }

        /// <summary>
        /// Removes an entry from the cache
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added</param>
        /// <returns>The item removed from the cache, or null if the entry was not found</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override object Remove(string key, string regionName = null)
        {
            if (String.IsNullOrWhiteSpace(key))
            {
                throw new ArgumentNullException("key");
            }

            object removed = this.Get(key);

            if (removed != null)
            {
                this._client.Delete(key);
            }

            return removed;
        }

        /// <summary>
        /// Inserts an entry into the cache, overwriting an existing entry with the same key.
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="value">The object to insert</param>
        /// <param name="policy">An object that contains eviction details for the cache entry.</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added</param>
        /// <exception cref="System.ArgumentNullException">key, value, or policy is null</exception>
        public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            this.Set(key, value, policy.AbsoluteExpiration);
        }

        /// <summary>
        /// Inserts an entry into the cache, overwriting an existing entry with the same key.
        /// </summary>
        /// <param name="item">The cache item to add.</param>
        /// <param name="policy">An object that contains eviction details for the cache entry.</param>
        /// <exception cref="System.ArgumentNullException">item or policy is null</exception>
        public override void Set(CacheItem item, CacheItemPolicy policy)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }

            if (policy == null)
            {
                throw new ArgumentNullException("policy");
            }

            this.Set(item.Key, item.Value, policy.AbsoluteExpiration.DateTime);
        }

        /// <summary>
        /// Inserts an entry into the cache, overwriting an existing entry with the same key.
        /// </summary>
        /// <param name="key">The unique identifier for the cache entry</param>
        /// <param name="value">The object to insert</param>
        /// <param name="absoluteExpiration">The fixed date and time at which the cache entry will expire.</param>
        /// <param name="regionName">Optional. A named region in the cache where the entry can be added</param>
        /// <exception cref="System.ArgumentNullException">key, value, absoluteExpiration is null</exception>
        public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            if (String.IsNullOrWhiteSpace(key))
            {
                throw new ArgumentNullException("key");
            }

            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (absoluteExpiration == null)
            {
                throw new ArgumentNullException("aboluteExpiration");
            }

            this._client.Set(key, value, absoluteExpiration.DateTime);
        }
    }
}
